package core;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.Graphics;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;

public class Img implements Serializable {
	// Class constants.
	public final static String JPEG_EXTENSION = "jpg";
	private final static String HASH_ALGORITHM = "SHA-1";
	private final static long serialVersionUID = 1;
	
	// Class attributes.
	private URL url = null;
	private String location = null;
	private String type = null;
	private String hash = null;
	private byte[] raw_data = null;
	
	/*
	 * Private functions for internal use.
	 */
	
	/**
	 * Function that transform an Img class object into a BufferedImage class
	 * object.
	 * 
	 * @param in: The Img object to transform.
	 * @return: A BufferedImage object representing the Img object.
	 */
	private BufferedImage Image2BufferedImage(Image in) {
		BufferedImage out;
		Graphics g;
		
		out = new BufferedImage(in.getWidth(null),
								in.getHeight(null),
								BufferedImage.TYPE_INT_RGB);
		g = out.getGraphics();
		g.drawImage(in, 0, 0, null);
		return out;
	}
	
	/**
	 * Returns the height of the Img.
	 * 
	 * @return: An integer representing the height of the image.
	 */
	private int getHeight() throws IOException {
		BufferedImage buff_img;
		
		buff_img = ImageIO.read(url);
		return buff_img.getHeight();
	}
	
	/**
	 * Returns the width of the Img.
	 * 
	 * @return: An integer representing the width of the image.
	 */
	private int getWidth() throws IOException {
		BufferedImage buff_img;
		
		buff_img = ImageIO.read(url);
		return buff_img.getWidth();
	}
	
	/**
	 * Determines whether the parameters passed means for a real rescaled
	 * image or not. For a real rescaled the asked dimensions should be
	 * different from the real ones and not zero. If one dimension is a
	 * negative value that ask for a proportional rescalation, but both
	 * dimensions cannot be negative.
	 * 
	 * @param asked_width: The asked width for the rescalation.
	 * @param asked_height: The asked height for the rescalation.
	 * @return: Returns True if and only if the asked dimensions are both not
	 * equals to the real ones AND if there is no combination of one dimension
	 * equal to the real one and the other negative AND if not both dimensions
	 * are negative. False elsewhere.
	 *
	 * @throws IOException: If there are problems reading the Img size.
	 */
	private boolean ask4scale(int asked_width, int asked_height)
	throws IOException {
		int width, height;
		
		width = getWidth();
		height = getHeight();
		
		// Asked for the same size as the real image.
		if (width == asked_width && height == asked_height)
			return false;
		// Asked for the same size in one dimension and a negative value in the
		// other one.
		else if ((width == asked_width && asked_height < 0) ||
				 (height == asked_height && asked_width < 0))
			return false;
		// Both dimensions are negative.
		else if (asked_width < 0 && asked_height < 0)
			return false;
		// Then they have gave me an specific width and a height (higher or
		// equal 0). That means, they have asked for real rescalation.
		else
			return true;
	}
	
	/**
	 * This function determines whether an String is a correct image HTTP
	 * content-type header by checking that it has the form "image/<mimetype>".
	 * Also it checks that the mimetype doesn't contains another a '/'
	 * character.
	 * 
	 * @param content_type: An string with the supposed content-type header.
	 * @return: Returns true if and only if the parameter is not null and is
	 * not empty and it starts with "image/" and it doesn't has the character
	 * '/' from there.
	 */
	private boolean isImageContentType(String content_type) {
		if (content_type == null)
			return false;
		else if (content_type.isEmpty())
			return false;
		else if (!content_type.startsWith("image/"))
			return false;
		else if (content_type.substring(6).contains("/"))
			return false;
		else
			return true;
	}
	
	/**
	 * Function that takes a path to an image and get its extension and
	 * returns it as if it were a content-type header of the http protocol.
	 * 
	 * @param fname:
	 * 		The filename of the image.
	 * @return:
	 * 		A String of the form image/<extension>.
	 */
	private String getExtension2ContentType(String fname) {
		String extension;
		int colon_index;
		
		colon_index = fname.lastIndexOf('.');
		extension = fname.substring(colon_index + 1, fname.length());
		return new String("image/" + extension);
	}
	
	/*
	 * Methods of the Img class.
	 */
	
	/**
	 * Empty constructor (needed for serialization).
	 */
	public Img() {}
	
	/**
	 * Img class constructor.
	 * 
	 * @param url: URL pointing to the image.
	 * @throws NoSuchAlgorithmException: In fact this exception should never
	 * thrown, internally this function uses objects from the MessageDigest
	 * library that can raise this exception, however we use a standard
	 * algorithm, this will be only raised if the library removes the algorithm
	 * we are using.
	 */
	public Img(URL url) throws NoSuchAlgorithmException {
		MessageDigest md;
		
		this.url = url;
		
		// Calculation of the hash of the image from the URL, it is stored as
		// an String with hexadecimal values ([0-9], [a-f] and [A-F]).
		md = MessageDigest.getInstance(HASH_ALGORITHM);
		md.update(url.toString().getBytes());
		this.hash = Util.byteArray2HexStr(md.digest());
	}
	
	/**
	 * Img class constructor. Creates an Img from a file.
	 * 
	 * @param fname:
	 * 		The filename of the image file.
	 * @throws NoSuchAlgorithmException:
	 * 		Should never be thrown.
	 */
	public Img(String fname) throws NoSuchAlgorithmException {
		MessageDigest md;
		
		location = fname;
		
		// Calculation of the hash of the image from the URL, it is stored as
		// an String with hexadecimal values ([0-9], [a-f] and [A-F]).
		md = MessageDigest.getInstance(HASH_ALGORITHM);
		md.update(location.getBytes());
		this.hash = Util.byteArray2HexStr(md.digest());
	}
	
	/**
	 * Download the image content from the Internet transformed to JPEG. Note
	 * that this method, as the getImage method, also download the image so
	 * using repeatedly these methods will cause downloading the image lot of
	 * times. getImage and getCompressImage should be used only one in order to
	 * reduce the network traffic and increase the program speed. Also notice
	 * that the byte array returned is always a JPEG, if you will use it to
	 * save it to a file you should not use the getType method to set the
	 * extension for the file since it could return something different from
	 * "jpeg" or "jpg" if the Img object is not of those types.
	 * 
	 * @return: A byte array containing the image in JPEG... ¡Always JPEG!
	 * @exception IOException: If there are problems retreiving the URL, for
	 * example if the pointed resource is not an image.
	 */
	
	public byte[] getCompressImage() throws IOException {
		return getCompressImage(getWidth(), getHeight());
	}

	/**
	 * Download the image content from the Internet escalating it and
	 * transforming it to a JPEG. You should read the commentaries on the
	 * getCompressImage method above.
	 * 
	 * @return:
	 * 		A byte array containing the image in JPEG... ¡Always JPEG!. If
	 * 		there are any problem it returns null.
	 */
	public byte[] getCompressImage(int maxWidth, int maxHeight) 
	throws IOException {
		Iterator<ImageWriter> jpeg_mod_iter;
		ImageWriter jpeg_mod;
		BufferedImage buff_img, buff_img2;
		Image image;
		ByteArrayOutputStream baos;
		ImageOutputStream ios;
		String content_type;

		if (url != null || location != null) {
			if (url != null)
				content_type = url.openConnection().getContentType();
			else
				content_type = getExtension2ContentType(location);
			
			if (content_type == null)
				throw new IllegalArgumentException("No existe cabecera" +
												   "Content-type");
			else if (content_type.isEmpty())
				throw new IllegalArgumentException("Cabecera Content-type " +
												   "vacía.");
			else if (!isImageContentType(content_type))
				throw new IllegalArgumentException("Cabecera Content-type no" +
												   "tiene formato" +
												   "\"image/<value>\"");
		
			// If the image is already a JPEG and the asked escalation is not for
			// a real escalation (check commentaries at ask4scale function) we
			// return the image byte array itself.
			if ((content_type.equals("image/jpeg") ||
				 content_type.equals("image/jpg")) &&
				!ask4scale(maxWidth, maxHeight)) {
				return getImage();
			}
		
			// Setting the image type.
			this.type = JPEG_EXTENSION;
		
			// Choose an ImageWriter that supports JPEG.
			jpeg_mod_iter = ImageIO.getImageWritersByFormatName(JPEG_EXTENSION);
			jpeg_mod = jpeg_mod_iter.next();
		
			// We create a BufferedImage to store and escalate the image.
			if (url != null)
				buff_img = ImageIO.read(this.url);
			else
				buff_img = ImageIO.read(new File(location));
			image = buff_img.getScaledInstance(maxWidth, maxHeight, 
											   Image.SCALE_DEFAULT);
		
			// The previous result is transformed again into a BufferedImage
			// object.
			buff_img2 = Image2BufferedImage(image);
		
			// We need an ImageOutputStream to later take the byte array without
			// much complexity, we create a ByteArrayOutputStream and from it the
			// ImageOutputStream.
			baos = new ByteArrayOutputStream();
			ios = ImageIO.createImageOutputStream(baos);
		
			// Preparing the compression, we set the output, compress it and write
			// it, all at once... ImageWriter magic.
			jpeg_mod.setOutput(ios);
			jpeg_mod.write(buff_img2);
			jpeg_mod.dispose();
			this.raw_data = baos.toByteArray();
			return this.raw_data;
		}
		else
			System.err.println("[WARN]: Image with url and location null.");
		
		return null;
	}
	
	/**
	 * Download the image content from the Internet (where the url is poiting).
	 * 
	 * @return: A byte array containing the image.
	 * @throws IOException: If there is a problem retrieving the image.
	 */
	public byte[] getImage() throws IOException {
		HttpURLConnection http_conn;
        InputStream is;
        String content_type;
        File img_fd;
        int content_len;
         
        if (url != null || location != null) {
        	// Check the correctness of the URL.
        	if (url != null) {
        		http_conn = (HttpURLConnection)url.openConnection();
        		if(http_conn.getResponseCode() != HttpURLConnection.HTTP_OK)
        			throw new FileNotFoundException();
        		content_type = http_conn.getContentType();
        		content_len = http_conn.getContentLength();
            	is = http_conn.getInputStream();
        	}
        	else {
        		content_type = getExtension2ContentType(location);
        		img_fd = new File(location);
        		content_len = (int)img_fd.length();
        		is = new FileInputStream(img_fd);
        	}
            
        	// Check the content-type header.
        	if (content_type == null)
        		throw new IllegalArgumentException("There is no content-type header.");
        	else if (content_type.isEmpty())
        		throw new IllegalArgumentException("Content-type header is empty.");
        	else if (!isImageContentType(content_type))
        		this.type = "image/jpeg";
            
        	// Setting the type of the image.
        	this.type = content_type.substring(6);
        	
        	// Reading the image.
        	this.raw_data = Util.recoverDatafromInputStream(is, content_len);
        	return this.raw_data;
        }
        else
        	System.err.println("[WARN]: Image with url and location null.");
        
        return null;
	}
	
	/**
	 * Returns the location of the image if it was build through a file. Null
	 * if the image was build through an url.
	 */
	public String getLocation() {
		return location;
	}

	/**
	 * Returns the url of the image if it was build through an URL Null
	 * if the image was build through a file.
	 */
	public URL getUrl() {
		return this.url;
	}

	/**
	 * Returns the image type. It needs the methods getImage or
	 * getCompressImage or setTypeFromFile to be called before this one, if
	 * not the result will be null.
	 * 
	 * @return: An String representing the type, i.e. "jpg", "gif", "png", etc.
	 */
	public String getType() {
		return this.type;
	}
	
	/**
	 * Returns the hash of the image, this hash is calculated from the String
	 * representing the URL.
	 * 
	 * @return: A String with the hash.
	 */
	public String getHash() {
		return this.hash;
	}
	
	/**
	 * Save image to a file.
	 * 
	 * @throws IOException 
	 */
	public void save2File(String fname) throws IOException {
		FileOutputStream fos;
		
		if (this.raw_data == null)
			throw new RuntimeException("Cannot save image " + fname +
									   ": image haven't been loaded.");
		
		fos = new FileOutputStream(fname);
		fos.write(this.raw_data);
		fos.close();
	}
	
	/**
	 * Some Img attributes are filled if the image already exists in a file in
	 * order to not download it again from its url with the getImage method.
	 * 
	 * @param fname: The file name where the image is located.
	 * @return: true si consiguio completarse todo el proceso y ahora la
	 * imagen tiene el atributo "extension", con lo cual ya se podria llamar a
	 * Returns True if and only if the fname has at least two '.' characters.
	 * The string between this two '.' is considered the type of the image and
	 * its stored in the type attribute. NOTE that this method is intended to
	 * be used with files already dusterized, those with extension .jpg.dust,
	 * .png.dust, etc... Use this method properly. 
	 */
	public boolean setTypeFromFile(String fname) {
		int first_dot, second_dot;
		
		// Search for the first '.'.
		first_dot = fname.indexOf(".");
		if (first_dot == -1)
			return false;
		
		// Search for the second '.'.
		second_dot = fname.indexOf(".", first_dot + 1);
		if (second_dot == -1)
			return false;
		
		// We take the string between both '.' characters.
		this.type = fname.substring(first_dot + 1, second_dot);
		return true;
	}
}
